import dbInit from "./db"
import randomstring from 'randomstring';

import eventTypes from './eventTypes/*';

async function init(app) {
    console.log("init persistent");
    //check saved events...
    let loadedFunctions = [], loadedInstances = [], idMap = [], loadedEvents = [], timeoutIDs = {};
    let db = await dbInit();

    let dep=0;

    function Instance(father, opts) {
        let myClosure = opts && opts.closure ? opts.closure : {};
        this.__myClosure__ = myClosure;
        this.__father__ = father;

        if (opts && opts.id) { // recover from db now
            this.__P$id__ = opts.id;
            loadedInstances[this.__P$id__] = this;

            async function asyncLoader() {
                async function load(piece) {
                    let ret;
                    // try {
                        if (typeof piece === "object") {
                            if (piece.fun) {
                                if (typeof idMap[piece.fun] === "undefined") {
                                    // console.log(`- load function ${piece.fun}`);
                                    ret= (await prepareFunction(piece.fun)).funObj;
                                }
                                else ret= idMap[piece.fun];
                            } else if (piece.obj) {
                                if (typeof idMap[piece.obj] === "undefined")
                                    ret= await loadObject(piece.obj);
                                else ret= idMap[piece.obj];
                            } else if (piece.sp) {
                                if (piece.sp == "null")
                                    ret= null;
                            }
                        }
                        else
                            ret= piece; //literal.
                    // }catch(ex){
                        // console.warn(`- load piece failed`, piece, "error:", ex, "ret:", ret);
                        // throw ex;
                    // }
                    // console.log("- loaded piece ", piece);
                    return ret;
                }

                async function loadObject(id) {
                    let json = await db.Objects.get(id);
                    //console.log(`obj${id}:${json}`);
                    let j = JSON.parse(json);
                    if (j.bi)  // todo: not distinguishing array iterated or property.
                        idMap[id] = [];
                    else
                        idMap[id] = {};
                    idMap[id].__P$id__ = id;
                    Object.defineProperty(idMap[id], "__P$id__", {
                        enumerable: false,
                        writable: true
                    });

                    for (let aVar in j.p) {
                        // console.log(`- load ${aVar} of ${id}`);
                        idMap[id][aVar] = await load(j.p[aVar]);
                        // console.log(`* load ${aVar} of ${id} OK`);
                    }
                    return idMap[id];
                }

                let json = JSON.parse(opts.instanceJSON);
                for (let aVar in json) {
                    // console.log(` - ${opts.id} load ${aVar}`)
                    // try {
                        myClosure[aVar] = await load(json[aVar]);
                    // }catch(e){
                    //     console.warn(` - ${opts.id} load ${aVar} failed! error:`, e);
                    //     throw "instance loading failure";
                    // }
                }
                // console.log(` - loading ${opts.id} completed.`)
            };
            this.__load__ = async () => await asyncLoader.apply(this);
        }
        else {
            this.__P$id__ = parseInt(Math.random() * 1000000000000);
            loadedInstances[this.__P$id__] = this;
        }

        this.get = () => {
            let closure = Object.assign(father.get(), myClosure);
            return closure;
        };
        this.getSelf= ()=>myClosure;

        this.setSelf = (what) => {
            for (let aVar in what)
                myClosure[aVar] = what[aVar];
            savingInstances.push(this);
        }
        this.setRecur = (what) => {
            //console.log(`setRecur: ${this.__P$id__}`);
            //console.log("setRecur before:",myClosure);
            for (let aVar in myClosure) {
                if (typeof what[aVar] !== "undefined")
                    myClosure[aVar] = what[aVar];
            }
            for (let aVar in myClosure)
                delete what[aVar];
            //console.log("setRecur after:",myClosure);
            savingInstances.push(this);
            if (father.__P$id__ != 0)
                father.setRecur(what);
            else
                this.setSelf(what);
        };
    }


    let rootInstance = {
        get: () => { return {};
        }, set: () => {
        }, __P$id__: 0
    };
    Instance.from = async (id) => {  //instances are always acyclic.
        if (typeof loadedInstances[id] !== "undefined")
            return loadedInstances[id];
        if (id == 0)
            return rootInstance;
        //console.log(`${dep}, from ${id}`);
        let c = await db.Instances.get(id);
        let p = {id, instanceJSON: c.instanceJSON};
        let ret;
        if (c.father === 0)
            ret = new Instance(rootInstance, p);
        else
            ret = new Instance(await Instance.from(c.father, p), p);
        if (ret.__load__) {
            //console.log(`loading instance ${id}`);
            await ret.__load__();
            //console.log(`instance ${id}:`,ret.__myClosure__);
        }
        dep-=1;
        //console.log(`return instance ${id}`);
        return ret;
    }

    let funClasses = [];

    function getFunClassFromCode(code, className) {
        let inputs = ["events", "persistent", "Instance"];
        //console.log(`generate className ${className}`);
        let funClass = new Function("events","persistent", "Instance", `return ${code}`)(subsystems.events, subsystems.persistent, Instance);
        //console.log(`+ generated className ${className}`);
        funClasses[className] = funClass;
        return funClass;
    }

    async function prepareFunction(id) {
        // dep+=1;
        //console.log(`${dep}, prepare function  ${id}...`);
        if (typeof loadedFunctions[id] !== "undefined") {
            // dep-=1;
            return {funObj:loadedFunctions[id]};
        }
        let fun = await db.Functions.get(id);
        let funClass = funClasses[fun];
        if (typeof funClass === "undefined")
            funClass = getFunClassFromCode(await db.Codes.get(fun.name), fun.name);
        let instance = await Instance.from(fun.instanceID);
        // console.log(`function ${id} recover instance ${fun.instanceID}`);
        let funObj = new funClass(instance);
        let {requireFunClass} = funObj.__P$info;
        for (let funClassName of requireFunClass)
            if (!funClasses[funClassName])
                getFunClassFromCode(await db.Codes.get(funClassName), funClassName);

        //funObj=()=>{console.warn(`invoke ${fun.name}:${id}`); console.warn(arguments); console.trace();}
        loadedFunctions[id] = funObj;
        idMap[id] = funObj;
        // console.log(`function  ${id} ok`);
        return {funObj};
    }

    let savingInstances = [], savingFunctions = [];
    let savingEvents = [];

    async function postProcessing() {
        console.log(`postProcessing`);

        let savedObjects = {};
        let savedInstances = {};
        let savedFunctions = {};

        // closure circular references.

        while (savingInstances.length > 0 || savingFunctions.length > 0) {
            let rSInstances = [];
            let rSFunctions = [];

            // ====== save instances
            function genJsonPiece(what, filler) {
                if (typeof what === "object") {
                    if (what == null)
                        filler({"sp": "null"});
                    else if (typeof savedObjects[what.__P$id__] === "undefined") {
                        let json = saveObject(what);
                        savedObjects[what.__P$id__] = json;
                        filler(json);
                    } else
                        filler(savedObjects[what.__P$id__]);
                }
                else if (typeof what === "function") {
                    if (typeof what.__P$info !== "undefined") {
                        if (typeof savedObjects[what.__P$id__] === "undefined") {
                            let json = saveFun(what);
                            savedObjects[what.__P$id__] = json;
                            filler(json);
                        } else
                            filler(savedObjects[what.__P$id__]);
                    }
                    else {
                        //console.warn(`Unpersistentable: ${what.toString()}`);//${what.toString().split("{")[0]}`)
                        filler({"sp":what.toString()});
                    }
                }
                else //literal.
                    filler(what);
            }

            function saveFun(fun) {
                if (typeof savedFunctions[fun.__P$id__] === "undefined") { // all function objects have created id.
                    rSFunctions.push(fun);
                    saveObject(fun);
                }
                return {fun: fun.__P$id__};
            }

            function saveObject(obj) { // user defined object.
                if (typeof savedObjects[obj.__P$id__] === "undefined") {
                    if (typeof obj.__P$id__ === "undefined") {
                        obj.__P$id__ = parseInt(Math.random() * 1000000000000);
                        Object.defineProperty(obj, "__P$id__", {
                            enumerable: false,
                            writable: true
                        });
                    }
                    savedObjects[obj.__P$id__] = true;
                    if (typeof obj.__save === "undefined") {
                        let json = {bi: typeof obj[Symbol.iterator] === 'function'}; // array?
                        json.p = {};
                        for (let property in obj)
                            if (!property.startsWith("__P$"))
                                genJsonPiece(obj[property], (piece) => json.p[property] = piece);
                        db.Objects.put({id: obj.__P$id__, json: JSON.stringify(json)});
                    } else {
                        // persist IO objects.
                    }
                }
                return {obj: obj.__P$id__};
            }

            for (let instance of savingInstances) {
                let instanceJSON = {};
                for (let _var in instance.__myClosure__)
                    genJsonPiece(instance.__myClosure__[_var], (piece) => instanceJSON[_var] = piece);

                await db.Instances.put({
                    id: instance.__P$id__,
                    father: instance.__father__.__P$id__,
                    instanceJSON: JSON.stringify(instanceJSON)
                }); //id==-1 update.
                savedInstances[instance.__P$id__] = true;
            }

            // ====== save functions.
            for (let fun of savingFunctions) {
                let {name, instance, code, id, requireFunClass} = fun.__P$info;
                let instanceID = instance.__P$id__; //assert instanceID != -1;
                if (typeof savedInstances[instanceID] === "undefined") {
                    rSInstances.push(instance);
                    rSFunctions.push(fun);
                    continue;
                }
                await db.Functions.put({id, name, instanceID});
                savedFunctions[id] = true;
                savedObjects[id] = true;
            }

            savingFunctions = rSFunctions;
            savingInstances = rSInstances
        }

        // events:
        for (let eventObj of savingEvents) {
            let {type, funID, opts, name} = eventObj;
            await db.Events.put({type, opts, name, funID})
        }

    }

    let subsystems = {
        persistent: {
            loadedFunctions,
            funClasses
        },
        events: {
            trigger: async (eventName, ...args) => { //
                let event = loadedEvents[eventName];
                if (typeof event === "undefined")
                    event = await db.Events.get(eventName);
                if (typeof event === "undefined")
                    throw "no such event";
                let {funID} = event;
                console.log(`trigger event ${eventName}, handler is ${funID}`);
                let {funObj} = await prepareFunction(funID);
                let retVal = funObj(...args);
                await postProcessing();
                console.log(`event ${eventName} handled, retVal:`, retVal);
                return retVal;
            },
            listen(eventDef, funObj) {
                //console.trace();
                console.log(`event listen:`,eventDef);
                let name = randomstring.generate();
                let funID = funObj.__P$id__;
                let type=eventDef.type, opts = {};
                if (typeof eventDef === "string") {
                    type = "user";
                    opts = {};
                    name = eventDef;
                } else if (typeof eventTypes[eventDef.type] !=="undefined")
                    opts=eventTypes[eventDef.type].listen(eventDef, name);
                else
                    throw "Invalid Event Type";
                loadedEvents[name] = {funID};
                if (typeof opts==="undefined") opts={};
                savingEvents.push({type, funID, opts, name});
                savingFunctions.push(funObj);
                return name;
            },
            async unlisten(name) {
                let eventObj = await db.Events.get(name);
                db.Events.unlisten(name);
                if (eventObj.type!=="user")
                    eventTypes[eventObj.type].unlisten(eventObj);
            }
        },
        Instance,
        db
    }

    async function runScript(scriptObj) {
        try {
            console.log("start logic script ...");
            scriptObj(new Instance(rootInstance), subsystems);

            await postProcessing();
            for (let name in funClasses){
                let funObj=new funClasses[name]();
                //console.log(`persistentable function ${name} saved`);
                let {code}=funObj.__P$info;
                await db.Codes.put({name,code});
            }
            console.log("logic script finished!");
        } catch (e) {
            console.warn("logic script error:");
            console.warn(e);
        }
        //db.dump();
    }


    for (let e in eventTypes)
        eventTypes[e].init(app, subsystems);

    app.persistent = {
        runScript,
        events: subsystems.events
    };
}

export default init;